﻿// Microsoft Public License (Ms-PL)
//
// This license governs use of the accompanying software. If you use the software, you accept this license.
// If you do not accept the license, do not use the software.
//
// 1. Definitions
//
// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here 
// as under U.S. copyright law.
//
// A "contribution" is the original software, or any additions or changes to the software.
//
// A "contributor" is any person that distributes its contribution under this license.
//
// "Licensed patents" are a contributor's patent claims that read directly on its contribution.
//
// 2. Grant of Rights
//
// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations 
// in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to 
// reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution 
// or any derivative works that you create.
//
// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in 
// section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed 
// patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution 
// in the software or derivative works of the contribution in the software.
//
// 3. Conditions and Limitations
//
// (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or 
// trademarks.
//
// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the 
// software, your patent license from such contributor to the software ends automatically.
//
// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and 
// attribution notices that are present in the software.
//
// (D) If you distribute any portion of the software in source code form, you may do so only under this license 
// by including a complete copy of this license with your distribution. If you distribute any portion of the 
// software in compiled or object code form, you may only do so under a license that complies with this license.
//
// (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express 
// warranties, guarantees or conditions. You may have additional consumer rights under your local laws which 
// this license cannot change. To the extent permitted under your local laws, the contributors exclude the 
// implied warranties of merchantability, fitness for a particular purpose and non-infringement.

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Services;
using CommerceBuilder.Common;
using CommerceBuilder.Orders;
using CommerceBuilder.Payments;
using CommerceBuilder.Products;
using CommerceBuilder.Reporting;
using CommerceBuilder.Shipping;
using CommerceBuilder.Users;
using CommerceBuilder.Utility;

namespace StructuredSolutions.ShippingAgent.AbleCommerce7
{
    [WebService(Namespace = DefaultNamespace,
        Description = "Shipping Agent for AbleCommerce 7.0 by Structured Solutions.")]
    public class AgentService : AgentServiceBase
    {
        #region Abstract Method Implementation

        protected override List<OrderResult> GetNewOrdersFromStore(string lastOrderNumber, int maxCount)
        {
            var orders = new List<OrderResult>();

            var criteria = new OrderSearchCriteria();

            // If lastOrderNumber is missing, then return orders from today.
            if (string.IsNullOrEmpty(lastOrderNumber))
                criteria.OrderDateStart = DateTime.Today;
            else
            {
                int startOrderId;
                if (int.TryParse(lastOrderNumber, out startOrderId))
                    criteria.OrderIdStart = startOrderId + 1;
                else
                    throw new ApplicationException(string.Format("Invalid last order number '{0}'.", lastOrderNumber));
            }

            var ds = new OrderDataSource();
            foreach (CommerceBuilder.Orders.Order cbOrder in ds.Search(criteria, "OrderId ASC"))
            {
                orders.Add(BuildOrderResult(cbOrder));
                if (orders.Count == maxCount) break;
            }

            // Sort the shipments by OrderId so that the most recent Order is last
            orders.Sort((x, y) => Convert.ToInt32(x.Order.OrderId).CompareTo(Convert.ToInt32(y.Order.OrderId)));

            return orders;
        }

        protected override OrderResult GetOrderFromStore(string orderId)
        {
            int cbOrderId;
            if (!int.TryParse(orderId, out cbOrderId))
                throw new ApplicationException("Invalid order ID.");

            var cbOrder = OrderDataSource.Load(cbOrderId, false);
            if (cbOrder == null)
                throw new ApplicationException("Invalid order ID.");

            return BuildOrderResult(cbOrder);
        }

        protected override InventoryResult GetOrderInventoryFromStore(string orderId)
        {
            int cbOrderId;
            if (!int.TryParse(orderId, out cbOrderId))
                throw new ApplicationException("Invalid order ID.");

            var cbOrder = OrderDataSource.Load(cbOrderId, false);
            if (cbOrder == null)
                throw new ApplicationException("Invalid order ID.");

            return BuildInventoryResult(cbOrder);
        }

        protected override List<OrderResult> GetOrdersByRangeFromStore(string startOrderNumber, string endOrderNumber, DateTime? startOrderDate, DateTime? endOrderDate, string lastOrderNumber, int maxCount)
        {
            var orders = new List<OrderResult>();

            var criteria = new OrderSearchCriteria();
            if (!string.IsNullOrEmpty(startOrderNumber))
            {
                int startOrderId;
                if (int.TryParse(startOrderNumber, out startOrderId))
                    criteria.OrderIdStart = startOrderId;
                else
                    throw new ApplicationException(string.Format("Invalid starting order number '{0}'.", startOrderNumber));
            }
            if (!string.IsNullOrEmpty(endOrderNumber))
            {
                int endOrderId;
                if (int.TryParse(endOrderNumber, out endOrderId))
                    criteria.OrderIdEnd = endOrderId;
                else
                    throw new ApplicationException(string.Format("Invalid ending order number '{0}'.", endOrderNumber));
            }
            if (startOrderDate.HasValue) criteria.OrderDateStart = startOrderDate.Value;
            if (endOrderDate.HasValue) criteria.OrderDateEnd = endOrderDate.Value;

            // Support paging
            if (!string.IsNullOrEmpty(lastOrderNumber))
            {
                int startOrderId;
                if (int.TryParse(lastOrderNumber, out startOrderId))
                    criteria.OrderIdStart = startOrderId + 1;
                else
                    throw new ApplicationException(string.Format("Invalid last order number '{0}'.", lastOrderNumber));
            }

            foreach (CommerceBuilder.Orders.Order cbOrder in new OrderDataSource().Search(criteria, "OrderId ASC"))
            {
                orders.Add(BuildOrderResult(cbOrder));
                if (orders.Count == maxCount) break;
            }

            // Sort the shipments by OrderId so that the highest OrderId is last
            orders.Sort((x, y) => Convert.ToInt32(x.Order.OrderId).CompareTo(Convert.ToInt32(y.Order.OrderId)));

            return orders;
        }

        /// <summary>
        /// Return one shipment from the store.
        /// </summary>
        /// <param name="orderId">Ignored by this implementation.</param>
        /// <param name="packageId">The AbleCommerce OrderShipment ID.</param>
        /// <returns>The Shipment corresponding to the AC OrderShipment.</returns>
        protected override PackageResult GetPackageFromStore(string orderId, string packageId)
        {
            int cbShipmentId;
            if (!int.TryParse(packageId, out cbShipmentId))
                throw new ApplicationException("Invalid package ID.");

            var cbShipment = OrderShipmentDataSource.Load(cbShipmentId, false);
            if (cbShipment == null)
                throw new ApplicationException("Invalid package ID.");

            return BuildPackageResult(cbShipment);
        }

        protected override List<TrackingProvider> GetTrackingProvidersFromStore()
        {
            var trackingProviders = new List<TrackingProvider>();
            foreach (ShipGateway shipGateway in ShipGatewayDataSource.LoadForStore())
            {
                var provider = new TrackingProvider();
                provider.Name = shipGateway.Name;
                provider.TrackingProviderId = shipGateway.ShipGatewayId.ToString();
                trackingProviders.Add(provider);
            }
            return trackingProviders;
        }

        protected override OrderResult UpdatePackageInStore(Update update)
        {
            int cbShipmentId;
            if (!int.TryParse(update.PackageId, out cbShipmentId))
                throw new ApplicationException("Invalid package ID.");

            var cbShipment = OrderShipmentDataSource.Load(cbShipmentId, false);
            if (cbShipment == null)
                throw new ApplicationException("Invalid package ID.");

            return BuildUpdateResult(update, cbShipment);
        }

        #endregion

        private static bool AddressesAreEqual(OrderShipment acShipment, ShippingAddress shippingAddress)
        {
            if (!StringsAreEqual(acShipment.ShipToAddress1, shippingAddress.Line1, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToAddress2, shippingAddress.Line2, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToCity, shippingAddress.City, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToCompany, shippingAddress.Company, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToCountryCode, shippingAddress.Country, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToEmail, shippingAddress.Email, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToFirstName, shippingAddress.FirstName, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToLastName, shippingAddress.LastName, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToPhone, shippingAddress.Phone, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToPostalCode, shippingAddress.PostalCode, StringComparison.CurrentCulture)) return false;
            if (!StringsAreEqual(acShipment.ShipToProvince, shippingAddress.Region, StringComparison.CurrentCulture)) return false;
            if (acShipment.ShipToResidence != shippingAddress.Residential) return false;
            return true;
        }

        private static Address BuildAddress(CommerceBuilder.Orders.Order cbOrder)
        {
            var address = new Address();
            address.AddressId = cbOrder.OrderId.ToString();
            address.City = StringOrNull(cbOrder.BillToCity);
            address.Company = StringOrNull(cbOrder.BillToCompany);
            address.Country = StringOrNull(cbOrder.BillToCountryCode);
            address.Email = StringOrNull(cbOrder.BillToEmail);
            address.Fax = StringOrNull(cbOrder.BillToFax);
            address.FirstName = StringOrNull(cbOrder.BillToFirstName);
            address.LastName = StringOrNull(cbOrder.BillToLastName);
            address.Line1 = StringOrNull(cbOrder.BillToAddress1);
            address.Line2 = StringOrNull(cbOrder.BillToAddress2);
            address.Phone = StringOrNull(cbOrder.BillToPhone);
            address.PostalCode = StringOrNull(cbOrder.BillToPostalCode);
            address.Region = StringOrNull(cbOrder.BillToProvince);
            return address;
        }

        private static Address BuildAddress(Warehouse cbWarehouse)
        {
            if (cbWarehouse == null) return null;

            var warehouse = new Address();
            warehouse.AddressId = cbWarehouse.WarehouseId.ToString();
            warehouse.City = StringOrNull(cbWarehouse.City);
            warehouse.Company = StringOrNull(cbWarehouse.Name);
            warehouse.Country = StringOrNull(cbWarehouse.CountryCode);
            warehouse.Email = StringOrNull(cbWarehouse.Email);
            warehouse.Fax = StringOrNull(cbWarehouse.Fax);
            warehouse.Line1 = StringOrNull(cbWarehouse.Address1);
            warehouse.Line2 = StringOrNull(cbWarehouse.Address2);
            warehouse.Phone = StringOrNull(cbWarehouse.Phone);
            warehouse.PostalCode = StringOrNull(cbWarehouse.PostalCode);
            warehouse.Region = StringOrNull(cbWarehouse.Province);

            return warehouse;
        }

        private static Customer BuildCustomer(User user)
        {
            if (user == null) return null;

            var customer = new Customer();
            customer.CustomerId = user.UserId.ToString();
            customer.CustomerUrl = BuildCustomerUrl("Members/MyAccount.aspx", "");
            customer.Email = user.Email;
            customer.MerchantUrl = BuildMerchantUrl("Admin/People/Users/EditUser.aspx", "UserId=" + user.UserId);
            customer.UserName = user.UserName;
            if (user.PrimaryAddress != null)
            {
                customer.FirstName = user.PrimaryAddress.FirstName;
                customer.LastName = user.PrimaryAddress.LastName;
            }
            return customer;
        }

        private static string BuildCustomerUrl(string path, string query)
        {
            try
            {
                var uri = new UriBuilder(Token.Instance.Store.StoreUrl);
                uri.Path += path;
                uri.Query = query;
                return uri.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static InventoryResult BuildInventoryResult(CommerceBuilder.Orders.Order cbOrder)
        {
            var inventory = new List<ItemCount>();
            if (Token.Instance.Store.EnableInventory && cbOrder.Items != null)
            {
                foreach (OrderItem cbItem in cbOrder.Items)
                {
                    if (cbItem.Product == null || cbItem.Product.InventoryMode == InventoryMode.None)
                        continue;

                    var itemCount = new ItemCount();
                    itemCount.ItemId = cbItem.OrderItemId.ToString();
                    itemCount.Count = cbItem.Product.InStock;
                    inventory.Add(itemCount);
                }
                if (inventory.Count > 0)
                {
                    var text = string.Format(OrderInventoryExportedText, HttpContext.Current == null ? "(no IP in request)" : HttpContext.Current.Request.UserHostAddress);
                    cbOrder.Notes.Add(new OrderNote(cbOrder.OrderId, 0, DateTime.UtcNow, text, NoteType.SystemPrivate));
                    cbOrder.Notes.Save();
                }
            }
            return new InventoryResult {Success = true, Inventory = inventory.Count > 0 ? inventory : null};
        }

        private static List<Item> BuildItems(OrderItemCollection orderItems)
        {
            var items = new List<Item>();
            var store = Token.Instance.Store;

            foreach (OrderItem orderItem in orderItems)
            {
                if (orderItem.OrderItemType != OrderItemType.Product) 
                    continue;

                var description = new List<string>();
                var item = new Item();
                item.Code = orderItem.Sku;
                item.ItemId = orderItem.OrderItemId.ToString();
                item.LineTotal = orderItem.ExtendedPrice.ToDecimal(null);
                item.Name = orderItem.Name;
                item.Price = orderItem.Price.ToDecimal(null);
                item.Quantity = orderItem.Quantity;
                item.Shippable = orderItem.Shippable != Shippable.No;

                if (!string.IsNullOrEmpty(orderItem.VariantName)) description.Add(orderItem.VariantName);
                if (orderItem.WrapStyle != null) description.Add("Gift wrap:" + orderItem.WrapStyle.Name);
                if (!string.IsNullOrEmpty(orderItem.GiftMessage)) description.Add("Gift wrap message: " + orderItem.GiftMessage);
                if (description.Count > 0) item.Description = string.Join("; ", description.ToArray());

                if (orderItem.Product != null)
                {
                    if (store.EnableInventory && orderItem.Product.InventoryMode != InventoryMode.None)
                        item.Inventory = orderItem.Product.InStock;

                    if (orderItem.Product.Manufacturer != null)
                    {
                        item.Manufacturer = new Supplier();
                        // No manufacturer email in AC
                        item.Manufacturer.Name = orderItem.Product.Manufacturer.Name;
                        item.Manufacturer.SupplierId = orderItem.Product.Manufacturer.ManufacturerId.ToString();
                    }

                    if (orderItem.Product.Vendor != null)
                    {
                        item.Vendor = new Supplier();
                        item.Vendor.Email = orderItem.Product.Vendor.Email;
                        item.Vendor.Name = orderItem.Product.Vendor.Name;
                        item.Vendor.SupplierId = orderItem.Product.VendorId.ToString();
                    }
                }
                items.Add(item);
            }
            return items.Count > 0 ? items : null;
        }

        private static string BuildMerchantUrl(string path, string query)
        {
            try
            {
                var uri = new UriBuilder(Token.Instance.Store.StoreUrl);
                if (Token.Instance.Store.Settings.SSLEnabled) uri.Scheme = "https";
                uri.Path += path;
                uri.Query = query;
                return uri.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static NamedAmountList BuildNonShippingCharges(OrderItemCollection cbOrderItems)
        {
            const string GiftWrap = "Gift Wrap";
            const string Handling = "Handling";
            const string Taxes = "Taxes";

            var charges = new NamedAmountList();
            foreach (OrderItem orderItem in cbOrderItems)
            {
                switch (orderItem.OrderItemType)
                {
                    case OrderItemType.GiftWrap:
                        charges[GiftWrap].Amount += orderItem.ExtendedPrice.ToDecimal(null);
                        break;
                    case OrderItemType.Handling:
                        charges[Handling].Amount += orderItem.ExtendedPrice.ToDecimal(null);
                        break;
                    case OrderItemType.Tax:
                        charges[Taxes].Amount += orderItem.ExtendedPrice.ToDecimal(null);
                        break;
                }
            }
            return charges.Count > 0 ? charges : null;
        }

        private static OrderResult BuildOrderResult(CommerceBuilder.Orders.Order cbOrder)
        {
            var order = new Order();

            // Build the "simple" fields that have little or no chance of failing
            order.CustomerUrl = BuildCustomerUrl("Members/MyOrder.aspx", "OrderId=" + cbOrder.OrderId);
            order.MerchantUrl = BuildMerchantUrl("Admin/Orders/ViewOrder.aspx", "OrderId=" + cbOrder.OrderId);
            order.OrderDate = cbOrder.OrderDate;
            order.OrderId = cbOrder.OrderId.ToString();
            order.OrderNumber = cbOrder.OrderId.ToString();
            order.OrderStatus = cbOrder.OrderStatus.DisplayName;
            order.OrderTotal = cbOrder.TotalCharges.ToDecimal(null);
            order.PaymentStatus = cbOrder.PaymentStatus.ToString();
            order.ShippingStatus = cbOrder.ShipmentStatus.ToString();

            try
            {
                order.BillingAddress = BuildAddress(cbOrder);
                order.Charges = BuildNonShippingCharges(cbOrder.Items);
                order.Customer = BuildCustomer(cbOrder.User);
                order.Items = BuildItems(cbOrder.Items);
                order.Payments = BuildPayments(cbOrder);
                order.Packages = BuildPackages(cbOrder);
                order.ShippingCharge = BuildShippingCharge(cbOrder.Items);

                var text = string.Format(OrderExportedText, HttpContext.Current == null ? "(no IP in request)" : HttpContext.Current.Request.UserHostAddress);
                cbOrder.Notes.Add(new OrderNote(cbOrder.OrderId, 0, DateTime.UtcNow, text, NoteType.SystemPrivate));
                cbOrder.Notes.Save();

                return new OrderResult { Success = true, Order = order };
            }
            catch (Exception ex)
            {
                return new OrderResult {Success = false, Message = ex.ToString(), Order = order};
            }
        }

        private static PackageResult BuildPackageResult(OrderShipment cbOrderShipment)
        {
            var package = new Package();
            package.CustomerUrl = BuildCustomerUrl("Members/MyOrder.aspx", "OrderId=" + cbOrderShipment.OrderId);
            package.MerchantUrl = BuildMerchantUrl("Admin/Orders/Shipments/Default.aspx", "OrderId=" + cbOrderShipment.OrderId);
            package.OrderId = cbOrderShipment.OrderId.ToString();
            package.PackageId = cbOrderShipment.OrderShipmentId.ToString();

            try
            {
                package.Destination = BuildShippingAddress(cbOrderShipment);
                package.Instructions = StringOrNull(cbOrderShipment.ShipMessage);
                package.Items = BuildItems(cbOrderShipment.OrderItems);
                package.MethodId = cbOrderShipment.ShipMethodId.ToString();
                package.MethodName = StringOrNull(cbOrderShipment.ShipMethodName);
                package.RateProviderId = BuildRateProviderId(cbOrderShipment.ShipMethod);
                package.RateProviderName = BuildRateProviderName(cbOrderShipment.ShipMethod);
                package.ShipDate = cbOrderShipment.IsShipped ? cbOrderShipment.ShipDate : default(DateTime?);
                // Use the last tracking provider assigned
                foreach (TrackingNumber trackingNumber in cbOrderShipment.TrackingNumbers)
                {
                    if (trackingNumber.ShipGateway == null) continue;
                    package.TrackingProviderId = trackingNumber.ShipGatewayId.ToString();
                    package.TrackingProviderName = trackingNumber.ShipGateway.Name;
                }
                // Use the default tracking provider if no tracking has been assigned
                if (string.IsNullOrEmpty(package.TrackingProviderName) && cbOrderShipment.ShipMethod.ShipGateway != null)
                {
                    package.TrackingProviderId = cbOrderShipment.ShipMethod.ShipGatewayId.ToString();
                    package.TrackingProviderName = cbOrderShipment.ShipMethod.ShipGateway.Name;
                }
                package.Warehouse = BuildAddress(cbOrderShipment.Warehouse);
                package.Weight = BuildWeight(cbOrderShipment.OrderItems);
                return new PackageResult { Success = true, Package = package };
            }
            catch (Exception ex)
            {
                return new PackageResult { Success = false, Message = ex.ToString(), Package = package };
            }
        }

        private static List<PackageResult> BuildPackages(CommerceBuilder.Orders.Order cbOrder)
        {
            var packages = new List<PackageResult>();
            foreach (OrderShipment cbOrderShipment in cbOrder.Shipments)
                packages.Add(BuildPackageResult(cbOrderShipment));
            return packages.Count > 0 ? packages : null;
        }

        private static NamedAmountList BuildPayments(CommerceBuilder.Orders.Order cbOrder)
        {
            const string Coupons = "Coupons";
            const string Discounts = "Discounts";

            var payments = new NamedAmountList();

            foreach (OrderItem orderItem in cbOrder.Items)
            {
                switch (orderItem.OrderItemType)
                {
                    case OrderItemType.Coupon:
                        payments[Coupons].Amount += orderItem.ExtendedPrice.ToDecimal(null);
                        break;
                    case OrderItemType.Discount:
                        payments[Discounts].Amount += orderItem.ExtendedPrice.ToDecimal(null);
                        break;
                }
            }

            foreach (Payment cbPayment in cbOrder.Payments)
            {
                var payment = new NamedAmount(cbPayment.PaymentMethodName, cbPayment.Amount.ToDecimal(null));
                payment.Description = string.Format("{0} - {1}", cbPayment.ReferenceNumber, cbPayment.PaymentStatus);
                payments[cbPayment.PaymentMethodName] = payment;
            }

            return payments.Count > 0 ? payments : null;
        }

        private static string BuildRateProviderId(ShipMethod shipMethod)
        {
            return shipMethod.ShipGateway == null
                ? (-shipMethod.ShipMethodTypeId).ToString()
                : shipMethod.ShipGateway.ShipGatewayId.ToString();
        }

        private static string BuildRateProviderName(ShipMethod shipMethod)
        {
            if (shipMethod.ShipGateway == null)
                switch (shipMethod.ShipMethodType)
                {
                    case ShipMethodType.FlatRate:
                        return "Flat Rate";
                    case ShipMethodType.WeightBased:
                        return "Vary by Weight";
                    case ShipMethodType.CostBased:
                        return "Vary by Cost";
                    case ShipMethodType.QuantityBased:
                        return "Vary by Quantity";
                    default:
                        return string.Empty;
                }
            return shipMethod.ShipGateway.Name;
        }

        private static ShippingAddress BuildShippingAddress(OrderShipment cbOrderShipment)
        {
            var address = new ShippingAddress();
            address.AddressId = cbOrderShipment.OrderShipmentId.ToString();
            address.City = StringOrNull(cbOrderShipment.ShipToCity);
            address.Company = StringOrNull(cbOrderShipment.ShipToCompany);
            address.Country = StringOrNull(cbOrderShipment.ShipToCountryCode);
            address.Email = StringOrNull(cbOrderShipment.ShipToEmail);
            address.Fax = StringOrNull(cbOrderShipment.ShipToFax);
            address.FirstName = StringOrNull(cbOrderShipment.ShipToFirstName);
            address.LastName = StringOrNull(cbOrderShipment.ShipToLastName);
            address.Line1 = StringOrNull(cbOrderShipment.ShipToAddress1);
            address.Line2 = StringOrNull(cbOrderShipment.ShipToAddress2);
            address.Phone = StringOrNull(cbOrderShipment.ShipToPhone);
            address.PostalCode = StringOrNull(cbOrderShipment.ShipToPostalCode);
            address.Region = StringOrNull(cbOrderShipment.ShipToProvince);
            address.Residential = cbOrderShipment.ShipToResidence;
            return address;
        }

        private static decimal BuildShippingCharge(OrderItemCollection cbOrderItems)
        {
            decimal shippingCharge = 0;

            foreach (OrderItem orderItem in cbOrderItems)
            {
                if (orderItem.OrderItemType == OrderItemType.Shipping)
                    shippingCharge += orderItem.ExtendedPrice.ToDecimal(null);
            }

            return shippingCharge;
        }

        private static OrderResult BuildUpdateResult(Update update, OrderShipment cbShipment)
        {
            var notes = new List<string>();

            if (update.Destination != null && !AddressesAreEqual(cbShipment, update.Destination))
            {
                cbShipment.ShipToAddress1 = update.Destination.Line1;
                cbShipment.ShipToAddress2 = update.Destination.Line2;
                cbShipment.ShipToCity = update.Destination.City;
                cbShipment.ShipToCompany = update.Destination.Company;
                cbShipment.ShipToCountryCode = update.Destination.Country;
                cbShipment.ShipToEmail = update.Destination.Email;
                cbShipment.ShipToFax = update.Destination.Fax;
                cbShipment.ShipToFirstName = update.Destination.FirstName;
                cbShipment.ShipToLastName = update.Destination.LastName;
                cbShipment.ShipToPhone = update.Destination.Phone;
                cbShipment.ShipToPostalCode = update.Destination.PostalCode;
                cbShipment.ShipToProvince = update.Destination.Region;
                cbShipment.ShipToResidence = update.Destination.Residential;
                if (cbShipment.Save() != SaveResult.Failed)
                    notes.Add(string.Format("Updated shipping address for shipment #{0}.", cbShipment.ShipmentNumber));
                else
                    return new OrderResult { Success = false, Message = "Cannot order shipping address." };
            }

            if (update.TrackingNumbers != null)
            {
                foreach (string trackingNumber in update.TrackingNumbers)
                {
                    if (string.IsNullOrEmpty(trackingNumber)) continue;

                    // Skip this tracking number if it is already in the list.
                    var found = false;
                    foreach (TrackingNumber number in cbShipment.TrackingNumbers)
                    {
                        if (number.TrackingNumberData.Equals(trackingNumber, StringComparison.CurrentCultureIgnoreCase))
                        {
                            found = true;
                            break;
                        }
                    }
                    if (found) continue;

                    var track = new TrackingNumber();
                    track.OrderShipmentId = cbShipment.OrderShipmentId;
                    int providerId;
                    if (!string.IsNullOrEmpty(update.TrackingProviderId) && int.TryParse(update.TrackingProviderId, out providerId))
                        track.ShipGatewayId = providerId;
                    track.TrackingNumberData = trackingNumber;
                    cbShipment.TrackingNumbers.Add(track);
                    if (cbShipment.Save() != SaveResult.Failed)
                        notes.Add(string.Format("Added tracking number {0} to shipment #{1}.", trackingNumber, cbShipment.ShipmentNumber));
                    else
                        return new OrderResult { Success = false, Message = "Cannot add tracking number." };
                }
            }

            if (update.ShipDate.HasValue)
            {
                if (cbShipment.ShipDate != update.ShipDate.Value)
                {
                    cbShipment.Ship(update.ShipDate.Value);
                    notes.Add(string.Format("Shipped shipment #{0}.", cbShipment.ShipmentNumber));
                }
            }

            if (notes.Count > 0)
            {
                var message = string.Join(" ", notes.ToArray());
                var text = string.Format(OrderUpdateText, message);
                cbShipment.Order.Notes.Add(new OrderNote(cbShipment.OrderId, 0, DateTime.UtcNow, text, NoteType.SystemPrivate));
                cbShipment.Order.Notes.Save();
                var cbOrder = OrderDataSource.Load(cbShipment.OrderId, false);
                var result = BuildOrderResult(cbOrder);
                result.Message = message;
                return result;
            }

            return new OrderResult { Success = true, Message = "No change." };
        }

        private static double BuildWeight(OrderItemCollection orderItems)
        {
            double weight = 0;

            foreach (OrderItem orderItem in orderItems)
            {
                if (orderItem.OrderItemType != OrderItemType.Product)
                    continue;

                if (orderItem.Shippable != Shippable.No)
                    weight += LocaleHelper.ConvertWeight(Token.Instance.Store.WeightUnit, orderItem.Weight, WeightUnit.Pounds).ToDouble(null);
            }

            return weight;
        }
    }
}